package net.oxygen.upload.service.impl;

import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.model.*;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.oxygen.upload.config.MinioProperties;
import net.oxygen.upload.dto.FileChunkDTO;
import net.oxygen.upload.enums.BizCodeEnum;
import net.oxygen.upload.exception.BizException;
import net.oxygen.upload.mapper.FileChunkMapper;
import net.oxygen.upload.model.FileChunkDO;
import net.oxygen.upload.request.FileChunkInitTaskReq;
import net.oxygen.upload.request.FileChunkMergeReq;
import net.oxygen.upload.request.FileUploadReq;
import net.oxygen.upload.service.IFileChunkService;
import net.oxygen.upload.service.IFileV2Service;
import net.oxygen.upload.service.IStorageService;
import net.oxygen.upload.util.CommonUtil;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 功能：文件分片服务接口实现类
 *
 * @version 1.0
 * @author: chenweiwei
 * @since: 2025/3/24 15:00
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class FileChunkServiceImpl implements IFileChunkService {

    private final FileChunkMapper fileChunkMapper;

    private final MinioProperties minioProperties;

    private final IFileV2Service fileV2Service;

    private final IStorageService storageService;

    @Override
    public FileChunkDTO initFileChunkTask(FileChunkInitTaskReq req) {
        // 根据文件原始名称生成Minio存储路径
        String objectKey = CommonUtil.getFilePath(req.getFileName());
        // 根据文件名（后缀）获取文件类型
        String contentType = MediaTypeFactory.getMediaType(objectKey).orElse(MediaType.APPLICATION_OCTET_STREAM)
                .toString();
        // 通过MINIO获取分片上传任务ID
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType(contentType);
        metadata.setContentLength(req.getTotalSize());
        InitiateMultipartUploadResult uploadResult = storageService
                .initMultipartUploadTask(minioProperties.getBucketName(), objectKey, metadata);
        String uploadId = uploadResult.getUploadId();
        // 存储分片上传任务信息
        int chunkNum = (int) Math.ceil(req.getTotalSize() * 1.0 / req.getChunkSize());
        FileChunkDO fileChunkDO = FileChunkDO.builder()
                .identifier(req.getIdentifier())
                .uploadId(uploadId)
                .fileName(req.getFileName())
                .bucketName(minioProperties.getBucketName())
                .objectKey(objectKey)
                .totalSize(req.getTotalSize())
                .chunkSize(req.getChunkSize())
                .chunkNum(chunkNum).build();
        fileChunkMapper.insert(fileChunkDO);
        return new FileChunkDTO(fileChunkDO).setFinished(false).setExistPartList(new ArrayList<>());
    }

    @Override
    public String genPreSignUploadUrl(String identifier, int partNumber) {
        log.warn("该方法已废弃，请使用 uploadPart 方法直接上传分片。identifier={}, partNumber={}", identifier, partNumber);
        FileChunkDO fileChunkDO = fileChunkMapper
                .selectOne(new QueryWrapper<FileChunkDO>().eq("identifier", identifier));
        if (fileChunkDO == null) {
            throw new BizException(BizCodeEnum.FILE_CHUNK_TASK_NOT_EXISTS);
        }
        Date expireDate = new Date(System.currentTimeMillis() + minioProperties.getPreSignUrlExpire());
        Map<String, Object> params = new HashMap<>();
        params.put("partNumber", partNumber);
        params.put("uploadId", fileChunkDO.getUploadId());
        URL preSignedUrl = storageService.genePreSignedUrl(fileChunkDO.getBucketName(), fileChunkDO.getObjectKey(),
                HttpMethod.PUT, expireDate, params);
        log.info("生成预签名URL地址 identifier={},partNumber={}, preSignedUrl={}", identifier, partNumber,
                preSignedUrl.toString());
        return preSignedUrl.toString();
    }

    @Override
    @Transactional
    public void mergeFileChunk(FileChunkMergeReq req) {
        // 检查分片上传任务是否存在
        FileChunkDO fileChunkDO = fileChunkMapper
                .selectOne(new QueryWrapper<FileChunkDO>().eq("identifier", req.getIdentifier()));
        if (fileChunkDO == null) {
            throw new BizException(BizCodeEnum.FILE_CHUNK_TASK_NOT_EXISTS);
        }
        // 检查所有分片是否已上传
        PartListing partListing = storageService.listMultipart(fileChunkDO.getBucketName(), fileChunkDO.getObjectKey(),
                fileChunkDO.getUploadId());
        List<PartSummary> parts = partListing.getParts();
        if (parts.size() != fileChunkDO.getChunkNum()) {
            throw new BizException(BizCodeEnum.FILE_CHUNK_NOT_ENOUGH);
        }
        // 计算文件实际大小
        long fileSize = parts.stream().mapToLong(PartSummary::getSize).sum();

        // 合并文件
        CompleteMultipartUploadResult result = storageService.mergeChunks(
                fileChunkDO.getBucketName(), fileChunkDO.getObjectKey(), fileChunkDO.getUploadId(),
                parts.stream()
                        .map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag()))
                        .collect(Collectors.toList()));

        // 判断是否成功，然后存储信息到数据库
        if (result.getETag() != null) {
            FileUploadReq fileUploadReq = FileUploadReq.builder()
                    .fileName(fileChunkDO.getFileName())
                    .identifier(req.getIdentifier())
                    .size(fileSize)
                    .file(null)
                    .build();

            // 存储文件信息到数据库
            fileV2Service.saveFile(fileUploadReq, fileChunkDO.getObjectKey());

            // 删除分片上传记录
            fileChunkMapper.deleteById(fileChunkDO.getId());

            log.info("分片合并成功");
        } else {
            throw new BizException(BizCodeEnum.FILE_CHUNK_MERGE_FAILED);
        }
    }

    @Override
    public FileChunkDTO listFileChunk(String identifier) {
        FileChunkDO task = fileChunkMapper.selectOne(new QueryWrapper<FileChunkDO>().eq("identifier", identifier));
        if (task == null) {
            throw new BizException(BizCodeEnum.FILE_CHUNK_TASK_NOT_EXISTS);
        }

        FileChunkDTO result = new FileChunkDTO(task);

        boolean objectExist = storageService.isObjectExist(task.getBucketName(), task.getObjectKey());
        if (!objectExist) {
            List<PartSummary> parts = storageService
                    .listMultipart(task.getBucketName(), task.getObjectKey(), task.getUploadId()).getParts();
            result.setExistPartList(parts);
            result.setFinished(parts.size() == task.getChunkNum());
        }

        // 前端根据返回值进行判断，如果finished为空，则表示分片上传任务已经完成，如果为true则表示分片已全部上传完成，需要调用合并接口，如果为false则表示仍有未完成的分片
        return result;
    }

    @Override
    public boolean uploadPart(String identifier, int partNumber, byte[] file) {
        try {
            // 检查分片上传任务是否存在
            FileChunkDO fileChunkDO = fileChunkMapper
                    .selectOne(new QueryWrapper<FileChunkDO>().eq("identifier", identifier));
            if (fileChunkDO == null) {
                throw new BizException(BizCodeEnum.FILE_CHUNK_TASK_NOT_EXISTS);
            }

            // 使用AmazonS3原生API上传分片
            UploadPartRequest uploadPartRequest = new UploadPartRequest()
                    .withBucketName(fileChunkDO.getBucketName())
                    .withKey(fileChunkDO.getObjectKey())
                    .withUploadId(fileChunkDO.getUploadId())
                    .withPartNumber(partNumber)
                    .withPartSize(file.length)
                    .withInputStream(new java.io.ByteArrayInputStream(file));

            // 执行上传并获取结果
            PartETag partETag = storageService.uploadPart(uploadPartRequest);

            log.info("分片上传成功 identifier={}, partNumber={}, etag={}", identifier, partNumber, partETag.getETag());
            return true;
        } catch (Exception e) {
            log.error("分片上传失败 identifier={}, partNumber={}, error={}", identifier, partNumber, e.getMessage(), e);
            return false;
        }
    }
}
